// Created by qiuwenchen on 2023/3/30.
//

/*
 * Tencent is pleased to support the open source community by making
 * WCDB available.
 *
 * Copyright (C) 2017 THL A29 Limited, a Tencent company.
 * All rights reserved.
 *
 * Licensed under the BSD 3-Clause License (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *       https://opensource.org/licenses/BSD-3-Clause
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.tencent.wcdb.core;

import com.tencent.wcdb.base.CppObject;
import com.tencent.wcdb.base.Value;
import com.tencent.wcdb.base.WCDBException;
import com.tencent.wcdb.orm.Field;
import com.tencent.wcdb.winq.Expression;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Database extends HandleORMOperation {
    private Database() {
    }

    /**
     * Init a database from path.
     * All database objects with same path share the same core. So you can create multiple database objects. WCDB will manage them automatically.
     * WCDB will not generate a sqlite db handle until the first operation, which is also called as lazy initialization.
     * @param path Path to your database
     */
    public Database(@NotNull String path) {
        cppObj = createDatabase(path);
    }

    private static native long createDatabase(String path);

    /**
     * Get the file path of the database.
     */
    @NotNull
    public String getPath() {
        return getPath(cppObj);
    }

    private static native String getPath(long self);

    /**
     * Set the tag of the database
     * The {@code WCDBException} generated by the database will carry its tag.
     * You can set the same tag for related databases for classification.
     * @param tag The tag of the database
     */
    public void setTag(long tag) {
        setTag(cppObj, tag);
    }

    private static native void setTag(long self, long tag);

    /**
     * Get the tag of the database. Tag is 0 by default.
     */
    public long getTag() {
        return getTag(cppObj);
    }

    private static native long getTag(long self);

    /**
     * Check whether the database can be opened.
     * Since WCDB is using lazy initialization, you can create an instance of {@code Database} even the database can't open.
     * @return true if an error occurs during sqlite db handle initialization.
     */
    public boolean canOpen() {
        return canOpen(cppObj);
    }

    private static native boolean canOpen(long self);

    /**
     * Check database is already opened.
     * @return true if the databse is opened
     */
    public boolean isOpened() {
        return isOpened(cppObj);
    }

    private static native boolean isOpened(long self);

    public interface CloseCallBack {
        /**
         * Triggered on database closed.
         * @throws WCDBException if any error occurs.
         */
        void onClose() throws WCDBException;
    }

    private static void onClose(@NotNull CloseCallBack callBack) throws WCDBException {
        callBack.onClose();
    }

    /**
     * Close the database.
     * Since Multi-threaded operation is supported in WCDB, other operations in different thread can open the closed database.
     * So this function can make sure database is closed in the callback. All other operations will be blocked until this function returns.
     * A close operation consists of 4 steps:
     *     1. blockade, which blocks all other operations.
     *     2. close, which waits until all sqlite db handles return and closes them.
     *     3. onClosed, which trigger the callback.
     *     4. unblokade, which unblocks all other operations.
     * You can simply call close: to do all steps above or call these separately.
     * Since this function will wait until all sqlite db handles return, it may lead to deadlock in some bad practice.
     * The key to avoid deadlock is to invalidate all alive {@code Handle} in current thread before you call this interface.
     * @param callBack on database is closed.
     * @throws WCDBException if any error occurs.
     */
    public void close(@Nullable CloseCallBack callBack) {
        close(cppObj, callBack);
    }

    /**
     * Close the database without a callback.
     * @see #close(CloseCallBack)
     * @throws WCDBException if any error occurs。
     */
    public void close() {
        close(cppObj, null);
    }

    private static native void close(long self, CloseCallBack callBack);

    /**
     * Blockade the database.
     * @see #close(CloseCallBack)
     */
    public void blockade() {
        blockade(cppObj);
    }

    private static native void blockade(long self);

    /**
     * Unblockade the database.
     * @see #close(CloseCallBack)
     */
    public void unblockade() {
        unblockade(cppObj);
    }

    private static native void unblockade(long self);

    /**
     * Check whether database is blockaded.
     * @see #close(CloseCallBack)
     * @return true if the database is blockaded
     */
    public boolean isBlockaded() {
        return isBlockaded(cppObj);
    }

    private static native boolean isBlockaded(long self);

    /**
     * Create a {@code Handle} for current database.
     * {@code Handle} is a wrapper for sqlite db handle of type {@code sqlite3*},
     * and the sqlite db handle is lazy initialized and will not be actually generated until the first operation on current handle takes place.
     * Note that All {@code Handle} created by the current database in the current thread will share the same sqlite db handle internally,
     * so it can avoid the deadlock between different sqlite db handles in some extreme cases.
     * @return A {@code Handle} object.
     */
    @NotNull
    public Handle getHandle() {
        return new Handle(this, false);
    }

    /**
     * Create a {@code Handle} for current database.
     * @see #getHandle()
     * @param writeHint a hint as to whether the current handle will be used to write content
     * @return A {@code Handle}object.
     */
    @Override
    @NotNull
    public Handle getHandle(boolean writeHint) {
        return new Handle(this, writeHint);
    }

    static native long getHandle(long self, boolean writeHint);

    /**
     * Purge all free memory of this database.
     * WCDB will cache and reuse some sqlite db handles to improve performance.
     * The max count of free sqlite db handles is same as the number of concurrent threads supported by the hardware implementation.
     * You can call it to save some memory.
     */
    public void purge() {
        purge(cppObj);
    }

    private static native void purge(long self);

    /**
     * Purge all free memory of all databases.
     * @see #purge()
     */
    public static native void purgeAll();

    /**
     * Get number of alive handles of current database.
     * @return number of alive handles.
     */
    public int getNumberOfAliveHandle() {
        return getNumberOfAliveHandle(cppObj);
    }

    private static native int getNumberOfAliveHandle(long self);

    /**
     * Use {@code sqlite3_release_memory} internally to free memory.
     * @param bytes Number of bytes need to be freed.
     */
    public static native void releaseSQLiteMemory(int bytes);

    /**
     * Use {@code sqlite3_soft_heap_limit64} internally to set soft limit of heap memory
     * @param limit Number of bytes to limit.
     */
    public static native void setSoftHeapLimit(long limit);

    public interface ProgressMonitor {
        /**
         * Triggered when operation progresses, you can return false to stop the operation.
         * @param percentage The percentage of current status.
         * @param increment The increment from last status.
         * @return True to continue current operation.
         */
        boolean onProgressUpdate(double percentage, double increment);
    }

    private static boolean onProgressUpdate(ProgressMonitor progress, double percentage, double increment) {
        return progress.onProgressUpdate(percentage, increment);
    }

    /**
     * Vacuum current database.
     * It can be used to vacuum a database of any size with limited memory usage.
     * @param monitor A progress monitor.
     * @throws WCDBException if any error occurs.
     */
    public void vacuum(@Nullable ProgressMonitor monitor) throws WCDBException {
        if(!vacuum(cppObj, monitor)) {
            throw createException();
        }
    }

    private static native boolean vacuum(long self, ProgressMonitor monitor);

    /**
     * The wrapper of {@code PRAGMA auto_vacuum}
     */
    public void enableAutoVacuum(boolean incremental) {
        enableAutoVacuum(cppObj, incremental);
    }

    private static native void enableAutoVacuum(long self, boolean incremental);

    /**
     * The wrapper of {@code PRAGMA incremental_vacuum}
     */
    public void incrementalVacuum(int pages) throws WCDBException {
        if(!incrementalVacuum(cppObj, pages)){
            throw createException();
        }
    }

    private static native boolean incrementalVacuum(long self, int pages);

    WCDBException createException() {
        return WCDBException.createException(getError(cppObj));
    }
    private static WCDBException createThreadedException() {
        return WCDBException.createException(getThreadedError());
    }

    private static native long getError(long self);
    private static native long getThreadedError();

    /**
     * Get paths to all database-related files.
     * @return all related paths
     */
    @NotNull
    public List<String> getPaths() {
        return getPaths(cppObj);
    }

    private static native List<String> getPaths(long self);

    /**
     * Remove all database-related files.
     * @throws WCDBException if any error occurs.
     */
    public void removeFiles() throws WCDBException {
        if(!removeFiles(cppObj)) {
            throw createException();
        }
    }

    private static native boolean removeFiles(long self);

    /**
     * Move all database-related files to another directory safely.
     * Since file operation is not atomic, There may be some accidents during this period.
     * For example, app may crash while db file is moved to destination and wal file is not.
     * Then none of destination and source contains the whole data.
     * This interface can make sure all of your data is in source or destination.
     * @param destination folder
     * @throws WCDBException if any error occurs.
     */
    public void moveFile(@NotNull String destination) throws WCDBException{
        if(!moveFile(cppObj, destination)) {
            throw createException();
        }
    }

    private static native boolean moveFile(long self, String destination);

    /**
     * Get the space used by the database files.
     * @return number of bytes used by database files.
     * @throws WCDBException if any error occurs.
     */
    public long getFileSize() throws WCDBException {
        long ret = getFileSize(cppObj);
        if(ret < 0) {
            throw createException();
        }
        return ret;
    }

    private static native long getFileSize(long self);

    /**
     * Main version of sqlcipher
     */
    public enum CipherVersion {
        defaultVersion, version1, version2, version3, version4
    }

    /**
     * Set cipher key for a database.
     * For an encrypted database, you must call it before all other operation.
     * The cipher page size defaults to 4096 in WCDB, but it defaults to 1024 in other databases.
     * So for an existing database created by other database framework, you should set it to 1024.
     * Otherwise, you'd better to use cipher page size with 4096 or simply call setCipherKey: interface to get better performance.
     * If your database is created with the default configuration of WCDB 1.0.x, please set cipherVersion to {@code CipherVersion.version3}.
     * @param key Cipher key.
     * @param pageSize The page size of database.
     * @param version Main version of sqlcipher.
     */
    public void setCipherKey(@NotNull byte[] key, int pageSize, CipherVersion version) {
        setCipherKey(cppObj, key, pageSize, version.ordinal());
    }

    /**
     * Set cipher key for a database.
     * @see #setCipherKey(long, byte[], int, int)
     * @param key Cipher key.
     * @param pageSize The page size of database
     */
    public void setCipherKey(@NotNull byte[] key, int pageSize) {
        setCipherKey(key, pageSize, CipherVersion.defaultVersion);
    }

    /**
     * Set cipher key for a database.
     * @see #setCipherKey(long, byte[], int, int)
     * @param key Cipher key.
     */
    public void setCipherKey(@NotNull byte[] key) {
        setCipherKey(key, 4096);
    }

    private static native void setCipherKey(long self, byte[] key, int pageSize, int version);

    /**
     * Force sqlcipher to operate with the default settings consistent with that major version number as the default.
     * It works the same as {@code PRAGMA cipher_default_compatibility}.
     * @param version The specified sqlcipher major version.
     */
    public static void setDefaultCipherVersion(CipherVersion version) {
        setDefaultCipherVersion(version.ordinal());
    }

    private static native void setDefaultCipherVersion(int version);


    public interface Config {
        /**
         * Triggered when a handle is opened, reused, invalidated or closed.
         * @param handle The handle need to be configured.
         * @throws WCDBException if any error occurs.
         */
        void onInvocation(@NotNull Handle handle) throws WCDBException;
    }

    private static boolean onConfig(long cppHandle, Config config) {
        Handle handle = new Handle(cppHandle, null);
        boolean ret = true;
        try {
            config.onInvocation(handle);
        } catch (WCDBException e) {
            ret = false;
        }
        return ret;
    }


    /**
     * Priority of config.
     * The higher the priority, the earlier it will be executed.
     * Note that the highest priority is only for cipher config.
     */
    public enum ConfigPriority {
        low, default_, high, highest
    }

    /**
     * Set config for this database.
     * Since WCDB is a multi-handle database, an executing handle will not apply this config immediately.
     * Instead, all handles will run this config before its next operation.
     * Note that if you want to add cipher config, please use {@code ConfigPriority.highest}.
     * @param configName name of config. It should be different from each other.
     * @param invocation a config will be triggered when a handle is open or reuse.
     * @param unInvocation a config will be triggered when a handle is invalidated or closed.
     * @param priority priority of config. It determines the execute order of configs.
     */
    public void setConfig(@NotNull String configName, @Nullable Config invocation, @Nullable Config unInvocation, ConfigPriority priority) {
        int cppPriority = 0;
        switch (priority) {
            case low:
                cppPriority = 100;
                break;
            case high:
                cppPriority = -100;
                break;
            case highest:
                cppPriority = -2147483648;
                break;
        }
        setConfig(cppObj, configName, invocation, unInvocation, cppPriority);
    }

    /**
     * Set config for this database.
     * @see #setConfig(String, Config, Config, ConfigPriority)
     * @param configName The name of config. It should be different from each other.
     * @param invocation A config will be triggered when a handle is open or reuse.
     * @param priority Priority of config. It determines the execute order of configs.
     */
    public void setConfig(@NotNull String configName, @Nullable Config invocation, ConfigPriority priority) {
        setConfig(configName, invocation, null, priority);
    }

    /**
     * Set config for this database with default priority.
     * @see #setConfig(String, Config, Config, ConfigPriority)
     * @param configName The name of config. It should be different from each other.
     * @param invocation A config will be triggered when a handle is open or reuse.
     */
    public void setConfig(@NotNull String configName, @Nullable Config invocation) {
        setConfig(configName, invocation, ConfigPriority.default_);
    }

    private static native void setConfig(long self, String configName, Config invocation, Config unInvocation, int priority);

    public static class PerformanceInfo {
        public int tablePageReadCount;
        public int tablePageWriteCount;
        public int indexPageReadCount;
        public int indexPageWriteCount;
        public int overflowPageReadCount;
        public int overflowPageWriteCount;
        public long costInNanoseconds;
    }

    public interface PerformanceTracer {
        /**
         * Triggered when a transaction or a normal sql ends.
         * @param tag Tag of the database where sql is executed
         * @param path Path of the database where sql is executed
         * @param handleId Id of the handle where sql is executed
         * @param sql The executed sql.
         * @param info Detail performance Info.
         */
        void onTrace(long tag, @NotNull String path, long handleId, @NotNull String sql, @NotNull PerformanceInfo info);
    }

    private static void onTracePerformance(PerformanceTracer tracer, long tag, String path, long handleId, String sql, long costInNanoseconds, int[] infoValues) {
        PerformanceInfo info = null;
        if(infoValues != null && infoValues.length == 6){
            info = new PerformanceInfo();
            info.tablePageReadCount = infoValues[0];
            info.tablePageWriteCount = infoValues[1];
            info.indexPageReadCount = infoValues[2];
            info.indexPageWriteCount = infoValues[3];
            info.overflowPageReadCount = infoValues[4];
            info.overflowPageWriteCount = infoValues[5];
            info.costInNanoseconds = costInNanoseconds;
        }
        assert info != null;
        tracer.onTrace(tag, path, handleId, sql, info);
    }

    /**
     * You can register a tracer to monitor the performance of all SQLs.
     * It returns
     *    1. Every SQL executed by the database.
     *    2. Time consuming in nanoseconds.
     *    3. Number of reads and writes on different types of db pages.
     *    4. Tag of database.
     *    5. Path of database.
     *    6. The id of the handle executing this SQL.
     * You should register trace before all db operations. Global tracer and db tracer do not interfere with each other.
     * Note that trace may cause WCDB performance degradation, according to your needs to choose whether to open.
     * @param tracer of performance.
     */
    public static native void globalTracePerformance(@Nullable PerformanceTracer tracer);


    /**
     * You can register a tracer to monitor the performance of all SQLs executed in the current database.
     * @see #globalTracePerformance(PerformanceTracer)
     * @param tracer of performance.
     */
    public void tracePerformance(@Nullable PerformanceTracer tracer) {
        tracePerformance(cppObj, tracer);
    }
    private static native void tracePerformance(long self, PerformanceTracer tracer);


    public interface SQLTracer {
        /**
         * Triggered when a SQL is executed.
         * @param tag Tag of the database where sql is executed.
         * @param path Path of the database where sql is executed.
         * @param handleId Id of the handle where sql is executed.
         * @param sql The executed sql.
         * @param info The detail info of executed sql. It is valid only when full sql trace is enable.
         */
        void onTrace(long tag, @NotNull String path, long handleId, @NotNull String sql, @NotNull String info);
    }

    private static void onTraceSQL(SQLTracer tracer, long tag,  String path, long handleId, String sql, String info) {
        tracer.onTrace(tag, path, handleId, sql, info);
    }

    /**
     * You can register a tracer to monitor the execution of all SQLs.
     * It returns:
     *    1. Every SQL executed by the database.
     *    2. Tag of database.
     *    3. Path of database.
     *    4. The id of the handle executing this SQL.
     *    5. Detailed execution information of SQL. It is valid only when full sql trace is enable.
     * You should register trace before all db operations. Global tracer and db tracer do not interfere with each other.
     * Note that tracer may cause WCDB performance degradation, according to your needs to choose whether to open.
     * @see #setFullSQLTraceEnable(boolean)
     * @param tracer A tracer of sql.
     */
    public static native void globalTraceSQL(@Nullable SQLTracer tracer);

    /**
     * You can register a tracer to monitor the execution of all SQLs executed in the current database.
     * You should register trace before all db operations. Global tracer and db tracer do not interfere with each other.
     * @see #globalTraceSQL(SQLTracer)
     * @param tracer A tracer of sql.
     */
    public void traceSQL(@Nullable SQLTracer tracer) {
        traceSQL(cppObj, tracer);
    }

    private static native void traceSQL(long self, SQLTracer tracer);

    /**
     * Enable to collect more SQL execution information in SQL tracer.
     * The detailed execution information of sql will include all bind parameters, step counts of {@code SELECT} statement,
     * last inserted rowid of {@code INSERT} statement, changes of {@code UPDATE} and {@code DELETE} statements.
     * These information will be returned in the last parameter of {@code SQLNotification}.
     * Collecting these information will significantly reduce the performance of WCDB,
     * please enable it only when necessary, and disable it when unnecessary.
     * @param enable A flag to enable full sql trace.
     */
    public void setFullSQLTraceEnable(boolean enable) {
        setFullSQLTraceEnable(cppObj, enable);
    }

    private static native void setFullSQLTraceEnable(long self, boolean enable);

    public interface ExceptionTracer {
        /**
         * Triggered synchronously before the database throws an WCDBException.
         * @param exception The exception to be thrown.
         */
        void onTrace(@NotNull WCDBException exception);
    }

    private static void onTraceException(ExceptionTracer tracer, long cppError) {
        tracer.onTrace(WCDBException.createException(cppError));
    }

    /**
     * You can register a reporter to monitor all database exceptions.
     * @param tracer The exception tracer.
     */
    public static native void globalTraceException(@Nullable ExceptionTracer tracer);

    /**
     * You can register a reporter to monitor all exceptions of current database.
     * @param tracer The exception tracer.
     */
    public void traceException(@Nullable ExceptionTracer tracer) {
        traceException(cppObj, tracer);
    }

    private static native void traceException(long self, ExceptionTracer tracer);

    /**
     * Operation type of current database.
     */
    public enum Operation {
        Create, SetTag, OpenHandle
    }

    public interface OperationTracer {
        /**
         * Triggered when a specific event of database occurs.
         * @param database The database where the event occurred.
         * @param operation The event type.
         * @param info Detail info of event. It is only valid when the event type is Operation.OpenHandle
         */
        void onTrace(@NotNull Database database, Operation operation, @NotNull HashMap<String, Value> info);
    }

    /**
     * The following are the keys in the info from the callback of database operation monitoring.
     */
    public static String OperationInfoKeyHandleCount = "HandleCount"; // The number of alive handles to the current database.
    public static String OperationInfoKeyOpenTime = "OpenTime"; // The time in microseconds spent to open and config the handle.
    public static String OperationInfoKeyOpenCPUTime = "OpenCPUTime"; // The cpu time in microseconds spent to open and config the handle.
    public static String OperationInfoKeySchemaUsage = "SchemaUsage"; // The memory in bytes used to store the schema in sqlite handle.
    public static String OperationInfoKeyTableCount = "TableCount"; // Number of tables in current database.
    public static String OperationInfoKeyIndexCount = "IndexCount"; // Number of indexes in current database.
    public static String OperationInfoKeyTriggerCount = "TriggerCount"; // Number of triggers in current database.

    private static void onTraceOperation(OperationTracer tracer, long cppDatabase, int cppOperation, long info) {
        Database database = new Database();
        database.cppObj = cppDatabase;
        Operation operation = Operation.Create;
        switch (cppOperation) {
            case 1:
                operation = Operation.SetTag;
                break;
            case 2:
                operation = Operation.OpenHandle;
                break;
        }
        HashMap<String, Value> javaInfo = new HashMap<>();
        enumerateInfo(javaInfo, info);
        tracer.onTrace(database, operation, javaInfo);
    }

    private static native void enumerateInfo(HashMap<String, Value> javaInfo, long cppInfo);
    private static void onEnumerateInfo(HashMap<String, Value> javaInfo,
                                        String key,
                                        int type,
                                        long intValue,
                                        double doubleValue,
                                        String stringValue) {
        if(type == 3) {
            javaInfo.put(key, new Value(intValue));
        } else if (type == 5) {
            javaInfo.put(key, new Value(doubleValue));
        } else if (type == 6)  {
            javaInfo.put(key, new Value(stringValue));
        }
    }

    /**
     * You can register a tracer to these database events:
     *    1. creating a database object for the first time;
     *    2. setting a tag on the database;
     *    3. opening a new database handle.
     * @see com.tencent.wcdb.core.Database.OperationTracer
     * @param tracer The tracer to event.
     */
    public static native void globalTraceDatabaseOperation(@Nullable OperationTracer tracer);

    public interface BusyTracer {
        /**
         * Triggered synchronously when the database operation is blocked.
         * @param tag Tag of database being busy.
         * @param path Path of database being busy.
         * @param tid The id of the thread being waited on.
         * @param sql The sql executing in the thread being waited on.
         */
        void onTrace(long tag, @NotNull String path, long tid, @NotNull String sql);
    }

    /**
     * You can register a tracer to database busy events.
     * It returns:
     *     1. Tag of database being busy.
     *     2. Path of database being busy.
     *     3. ID of the thread being waited on.
     *     4. SQL executing in the thread being waited on.
     * Since the tracer will be called back synchronously when the database operation is blocked and times out,
     * you can neither directly access the busy database nor perform heavy operation in the tracer.
     * @param tracer The tracer of busy event.
     * @param timeOut Timeout in seconds for blocking database operation.
     */
    public static native void globalTraceBusy(@Nullable BusyTracer tracer, double timeOut);

    private static void onBusyTrace(BusyTracer tracer, long tag, String path, long tid, String sql) {
        tracer.onTrace(tag, path, tid, sql);
    }

    /**
     * Setup tokenizer with name for current database.
     * You can use the builtin tokenizers defined in {@link com.tencent.wcdb.fts.BuiltinTokenizer}.
     * @see com.tencent.wcdb.fts.BuiltinTokenizer
     * @param tokenizer Name of tokenizer.
     */
    public void addTokenizer(@NotNull String tokenizer) {
        addTokenizer(cppObj, tokenizer);
    }

    private static native void addTokenizer(long self, String tokenizer);

    /**
     * Configure the mapping relationship between Chinese characters and their pinyin.
     * It is designed for the builtin pinyin tokenizer.
     * @see com.tencent.wcdb.fts.BuiltinTokenizer#Pinyin
     * @param pinyinDict The keys are Chinese characters, and the values are the corresponding pinyin lists.
     */
    public static void configPinyinDict(@NotNull Map<String, List<String>> pinyinDict) {
        String[] keys = pinyinDict.keySet().toArray(new String[0]);
        if(keys.length == 0){
            return;
        }
        String[][] values = new String[keys.length][];
        for(int i = 0; i < keys.length; i++) {
            List<String> pinyin = pinyinDict.get(keys[i]);
            if(pinyin == null){
                continue;
            }
            values[i] = pinyin.toArray(new String[0]);
        }
        configPinyinDict(keys, values);
    }

    private static native void configPinyinDict(String[] keys, String[][] values);

    /**
     * Configure the mapping relationship between traditional Chinese characters and simplified Chinese characters.
     * This is designed for the tokenizers configured with SimplifyChinese.
     * @see com.tencent.wcdb.fts.BuiltinTokenizer.Parameter#SimplifyChinese
     * @param traditionalChineseDict The keys are simplified Chinese characters, and the values are the corresponding Traditional Chinese characters.
     */
    public static void configTraditionalChineseDict(@NotNull Map<String, String> traditionalChineseDict) {
        String[] keys = traditionalChineseDict.keySet().toArray(new String[0]);
        if(keys.length == 0){
            return;
        }
        String[] values = new String[keys.length];
        for(int i = 0; i < keys.length; i++) {
            values[i] = traditionalChineseDict.get(keys[i]);
        }
        configTraditionalChineseDict(keys, values);
    }

    private static native void configTraditionalChineseDict(String[] keys, String[] values);

    /**
     * Setup auxiliary function with name for current database.
     * You can use the auxiliary function defined in {@link com.tencent.wcdb.fts.BuiltinFTSAuxiliaryFunction}.
     * @param auxiliaryFunction The function name.
     */
    public void addAuxiliaryFunction(@NotNull String auxiliaryFunction) {
        addAuxiliaryFunction(cppObj, auxiliaryFunction);
    }

    private static native void addAuxiliaryFunction(long self, String auxiliaryFunction);

    public interface CorruptionNotification {
        /**
         * Triggered when a database is confirmed to be corrupted.
         * @param database The corrupted database.
         */
        void onCorrupted(@NotNull Database database);
    }

    private static void onCorrupted(CorruptionNotification monitor, long cppDatabase) {
        Database database = new Database();
        database.cppObj = cppDatabase;
        monitor.onCorrupted(database);
    }

    /**
     * Register database corruption notification callback.
     * If the current database reports an error of {@code SQLITE_CORRUPT} or {@code SQLITE_NOTADB} during operation,
     * WCDB will asynchronously use {@code PRAGMA integrity_check} to check whether this database is truly corrupted.
     * Once confirmed, WCDB will notify you through the callback registered by this function.
     * In the callback, you can delete the corrupted database or try to repair the database.
     * @param monitor The corruption monitor.
     */
    public void setNotificationWhenCorrupted(@Nullable CorruptionNotification monitor) {
        setNotificationWhenCorrupted(cppObj, monitor);
    }

    private static native void setNotificationWhenCorrupted(long self, CorruptionNotification monitor);

    /**
     * Check if the current database is corrupted.
     * Since this function will check all the contents of the database until it finds something corrupted, it may take a lot of time to come to a conclusion.
     * @return True if the current database is corrupted.
     */
    public boolean checkIfCorrupted() {
        return checkIfCorrupted(cppObj);
    }

    private static native boolean checkIfCorrupted(long self);

    /**
     * Check if the current database is already observed corrupted by WCDB.
     * It only checks a internal flag of current database.
     * @return True if the current database is already corrupted.
     */
    public boolean checkIfIsAlreadyCorrupted() {
        return checkIfIsAlreadyCorrupted(cppObj);
    }

    private static native boolean checkIfIsAlreadyCorrupted(long self);

    /**
     * Enable database to automatically backup itself after there are updates.
     * The backup content mainly includes the SQL statements related to table creation and all leaf page numbers of each table in database.
     * And the backup file name is the database file name plus "-first.material" and "-last.material" suffixes.
     * Auto-backup do not degrade IO performance of the database.
     * @param enable A flag to enable auto-backup.
     */
    public void enableAutoBackup(boolean enable) {
        enableAutoBackup(cppObj, enable);
    }

    private static native void enableAutoBackup(long self, boolean enable);

    /**
     * Back up the database manually.
     * @see #enableAutoBackup(boolean)
     * @throws WCDBException if any error occurs.
     */
    public void backup() throws WCDBException {
        if(!backup(cppObj)) {
            throw createException();
        }
    }

    private static native boolean backup(long self);

    public interface BackupFilter {
        /**
         * Triggered before backup of each table.
         * Return false to skip backup of this table.
         * @param tableName The name of tables need to be backed up.
         * @return True if current table needs to be backed up.
         */
        boolean tableShouldBeBackup(@NotNull String tableName);
    }

    private static boolean checkTableShouldBeBackup(BackupFilter filter, String tableName) {
        return filter.tableShouldBeBackup(tableName);
    }

    /**
     * Set a filter to tell which table should be backed up.
     * The filter will be called for every table in database.
     * If there are some tables that do not need to be backed up, return false when these table names are passed into the filter.
     * @param filter A table filter.
     */
    public void filterBackup(@Nullable BackupFilter filter) {
        filterBackup(cppObj, filter);
    }

    private static native void filterBackup(long self, BackupFilter filter);

    /**
     * Recover data from a corruped db.
     * If there is a valid backup of this database, most of the uncorrupted data can be recovered,
     * otherwise WCDB will try to read all the data that can still be accessed, starting from the first page of the database.
     * In the extreme case where this database is not backed up and the first page is corrupted, no data can be recovered.
     * Since repairing a database requires reading all uncorrupted data and writing them to a new database, it may take a lot of time to finish the job.
     * During this period, you'd better display a loading view on the screen and present the processing percentage.
     * @param monitor A progress monitor.
     * @return Percentage of repaired data. 0 or less then 0 means data recovery failed. 1 means data is fully recovered.
     * @throws WCDBException if any error occurs.
     */
    public double retrieve(@Nullable ProgressMonitor monitor) throws WCDBException {
        double score = retrieve(cppObj, monitor);
        if(score < 0) {
            throw createException();
        }
        return score;
    }

    private static native double retrieve(long self, ProgressMonitor monitor);

    /**
     * Move the current database to a temporary directory and create a new database at current path.
     * This method is designed for conditions where the database is corrupted and cannot be repaired temporarily.
     * It can deposit the corrupted database to a new location and create a temporary database for the user.
     * The contents of the deposited database will be restored when you recover the database.
     * This function can be called multiple times without loss of data.
     * @throws WCDBException if any error occurs.
     */
    public void deposit() throws WCDBException {
        if(!deposit(cppObj)) {
            throw createException();
        }
    }

    private static native boolean deposit(long self);

    /**
     * Remove deposited data.
     * @see #deposit()
     * @throws WCDBException if any error occurs.
     */
    public void removeDepositedFiles() throws WCDBException {
        if(!removeDepositedFiles(cppObj)) {
            throw createException();
        }
    }

    private static native boolean removeDepositedFiles(long self);

    /**
     * Check whether there is deposited data.
     * @see #deposit()
     * @return True if deposited data exists.
     */
    public boolean containDepositedFiles() {
        return containDepositedFiles(cppObj);
    }

    private static native boolean containDepositedFiles(long self);

    /**
     * Checkpoint the current database with passive mode.
     * @throws WCDBException if any error occurs.
     */
    public void passiveCheckpoint() throws WCDBException {
        if(!passiveCheckpoint(cppObj)) {
            throw createException();
        }
    }

    private static native boolean passiveCheckpoint(long self);

    /**
     * Checkpoint the current database with truncate mode.
     * @throws WCDBException if any error occurs.
     */
    public void truncateCheckpoint() throws WCDBException {
        if(!truncateCheckpoint(cppObj)) {
            throw createException();
        }
    }

    private static native boolean truncateCheckpoint(long self);

    /**
     * Enable to auto checkpoint current database.
     * By default, WCDB performs checkpoints on all databases asynchronously.
     * This method is only used for testing.
     * Note that auto-backup of the database also relies on auto-checkpoint.
     * The auto-backup will also disable when auto-checkpoint is disable.
     * @param enable A flag to enable auto-checkpoint.
     */
    public void setAutoCheckpointEnable(boolean enable) {
        setAutoCheckpointEnable(cppObj, enable);
    }

    private static native void setAutoCheckpointEnable(long self, boolean enable);

    public static class MigrationInfo {
        public String table;                // Target table of migration
        public String sourceTable;          // Source table of migration
        public Expression filterCondition;  // Filter condition of source table
    }

    public static native void setAutoCheckpointMinFrames(int frames);

    public interface MigrationFilter {
        /**
         * Triggered at any time when WCDB needs to know whether a table in the current database needs to migrate data,
         * mainly including creating a new table, reading and writing a table, and starting to migrate a new table.
         * If the current table does not need to migrate data, you need to set the sourceTable in {@link MigrationInfo} to empty string.
         * @param info Migration configuration info of a table.
         */
        void filterMigrate(@NotNull MigrationInfo info);
    }
    private static void filterMigrate(MigrationFilter filter, long cppInfoSetter, long cppInfo, String table) {
        MigrationInfo info = new MigrationInfo();
        info.table = table;
        filter.filterMigrate(info);
        setMigrationInfo(cppInfoSetter, cppInfo, info.sourceTable, CppObject.get(info.filterCondition));
    }

    private static native void setMigrationInfo(long cppInfoSetter, long cppInfo, String sourceTable, long filterCondition);

    /**
     * Configure which tables in the current database need to migrate data, and the source table they need to migrate data from.
     * Once configured, you can treat the target table as if it already has all the data of the source table, and can read and write these data through the target table.
     * WCDB will internally convert your CRUD operations on the target table into the CRUD operations on both the target table and the source table appropriately.
     * You neither need to be aware of the existence of the source table, nor care about the progress of data migration.
     * Note that The column definition of the target table must be exactly the same as the column definition of the source table.
     * The database does not record the state of the migration to disk, so if you have data to migrate, you need to use this function to configure the migration before executing any statements on current database.
     * If the source table is in the current database, you can set sourcePath to empty string.
     * If the source table is not in the current database, the database containing the source table will be attached to the current database before the migration is complete. After migration, source tables will be dropped.
     * @param sourcePath Path of the source database for migration. It must be null if this is not a cross-database migration.
     * @param sourceCipher Cipher of the source database for migration. It must be null if this is not a cross-database migration.
     * @param filter A filter to config migration relationship of all tables.
     */
    public void addMigrationSource(@NotNull String sourcePath, @Nullable byte[] sourceCipher, @NotNull MigrationFilter filter) {
        addMigrationSource(cppObj, sourcePath, sourceCipher, filter);
    }


    /**
     * Configure which tables in the current database need to migrate data, and the source table they need to migrate data from.
     * @see #addMigrationSource(String, byte[], MigrationFilter)
     * @param sourcePath Path of the source database for migration. It must be null if this is not a cross-database migration.
     * @param filter A filter to config migration relationship of all tables.
     */
    public void addMigrationSource(@NotNull String sourcePath, @NotNull MigrationFilter filter) {
        addMigrationSource(sourcePath, null, filter);
    }

    private static native void addMigrationSource(long self, String sourcePath, byte[] sourceCipher, MigrationFilter filter);

    /**
     * Manually spend about 0.01 sec. to migrate data.
     * You can call this function periodically until all data is migrated.
     * @throws WCDBException if any error occurs.
     */
    public void stepMigration() throws WCDBException {
        if(!stepMigration(cppObj)) {
            throw createException();
        }
    }

    private static native boolean stepMigration(long self);

    /**
     * Configure the database to automatically step migration every two seconds.
     * @param enable A flag to enable auto-migration.
     */
    public void enableAutoMigration(boolean enable) {
        enableAutoMigration(cppObj, enable);
    }

    private static native void enableAutoMigration(long self, boolean enable);

    public interface MigrationNotification {
        /**
         * Triggered when a table or a database is migrated completely.
         * When a table is migrated successfully, tableInfo will carry the information of the table.
         * When a database is migrated, info has no value.
         * @param database The target database being migrated.
         * @param info Table info of migration.
         */
        void onMigrated(@NotNull Database database, @Nullable MigrationInfo info);
    }

    private static void onTableMigrated(MigrationNotification notification, long cppDatabase, String table, String sourceTable) {
        Database database = new Database();
        database.cppObj = cppDatabase;
        MigrationInfo info = null;
        if(table != null && table.length() > 0) {
            info = new MigrationInfo();
            info.table = table;
            info.sourceTable = sourceTable;
        }
        notification.onMigrated(database, info);
    }

    /**
     * Register a callback for migration notification.
     * The callback will be called when each table completes the migration.
     * @param notification A notification callback.
     */
    public void setNotificationWhenMigrated(@Nullable MigrationNotification notification) {
        setNotificationWhenMigrated(cppObj, notification);
    }

    private static native void setNotificationWhenMigrated(long self, MigrationNotification notification);

    /**
     * Check if all tables in the database has finished migration.
     * It only check an internal flag of database.
     * @return True if all tables in the database has finished migration.
     */
    public boolean isMigrated() {
        return isMigrated(cppObj);
    }

    private static native boolean isMigrated(long self);

    /**
     * Train a zstd formalized dict with a set of sample strings.
     * Note that the total size of all samples cannot exceed 4G.
     * @param samples sample strings.
     * @param dictId Id of the result id. DictId must be an integer between 1 and 999.
     * @return A new zstd dict.
     * @throws WCDBException if any error occurs.
     */
    @NotNull
    public static byte[] trainDictWithString(@NotNull List<String> samples, byte dictId) throws WCDBException {
        byte[] dict = trainDict(samples.toArray(new String[0]), dictId);
        if(dict == null || dict.length == 0) {
            throw createThreadedException();
        }
        return dict;
    }

    private static native byte[] trainDict(String[] samples, byte dictId);

    /**
     * Train a zstd formalized dict with a set of sample datas.
     * Note that the total size of all samples cannot exceed 4G.
     * @param samples sample data.
     * @param dictId Id of the result id. DictId must be an integer between 1 and 999.
     * @return A new zstd dict.
     * @throws WCDBException if any error occurs.
     */
    @NotNull
    public static byte[] trainDictWithData(@NotNull List<byte[]> samples, byte dictId) throws WCDBException {
        byte[] dict = trainDict(samples.toArray(new byte[0][]), dictId);
        if(dict == null || dict.length == 0) {
            throw createThreadedException();
        }
        return dict;
    }

    private static native byte[] trainDict(byte[][] samples, byte dictId);

    /**
     * Register a zstd dict in to WCDB.
     * You must register a dict before using it.
     * @param dict Dict data
     * @param dictId Id of the dict.
     * @throws WCDBException if any error occurs.
     */
    public static void registerDict(@NotNull byte[] dict, byte dictId) throws WCDBException {
        if(!nativeRegisterDict(dict, dictId)){
            throw createThreadedException();
        }
    }

    private static native boolean nativeRegisterDict(byte[] dict, byte dictId);

    public static final long DictDefaultMatchValue = Long.MAX_VALUE;
    public static class CompressionInfo {
        public String table; // The table to be compressed.

        /**
         * Configure to compress all data in the specified column with the default zstd compression algorithm.
         * @param field The column to be compressed.
         */
        public <T> void addZSTDNormalCompress(@NotNull Field<T> field) {
            Database.addZSTDNormalCompress(cppInfo, CppObject.get(field));
        }

        /**
         * Configure to compress all data in the specified column with a registered zstd dict.
         * @param field The column to be compressed.
         * @param dictId The id of zstd dict to use.
         */
        public <T> void addZSTDDictCompress(@NotNull Field<T> field, byte dictId) {
            Database.addZSTDDictCompress(cppInfo, CppObject.get(field), dictId);
        }

        /**
         * Enable to replace original compression format.
         * After activation, you can use {@link Database#stepCompression()} and {@link Database#enableAutoCompression(boolean)}
         * to recompress the existing data with the new compression configuration.
         */
        public <T> void enableReplaceCompression() {
            Database.enableReplaceCompression(cppInfo);
        }

        /**
         * Configure to compress all data in the specified column with multi registered zstd dict.
         * Which dict to use when compressing is based on the value of the specified matching column.
         * Note that you can use {@code DictDefaultMatchValue} to specify a default dict.
         * @param field The column to be compressed.
         * @param matchField The column used to distinguish different dict.
         * @param dicts The matching relationship between the dict id and the value of the matching column.
         */
        public <T> void addZSTDMultiDictCompress(@NotNull Field<T> field, @NotNull Field<T> matchField, @NotNull Map<Long, Byte> dicts) {
            long[] values = new long[dicts.size()];
            byte[] dictIds = new byte[dicts.size()];
            int index = 0;
            for(Map.Entry<Long, Byte> entry : dicts.entrySet()) {
                values[index] = entry.getKey();
                dictIds[index] = entry.getValue();
                index++;
            }
            Database.addZSTDMultiDictCompress(cppInfo, CppObject.get(field), CppObject.get(matchField), values, dictIds);
        }

        CompressionInfo(long cppInfo, String table) {
            this.cppInfo = cppInfo;
            this.table = table;
        }
        private long cppInfo;
    }

    private static native void addZSTDNormalCompress(long cppInfo, long cppColumn);
    private static native void addZSTDDictCompress(long cppInfo, long cppColumn, byte dictId);
    private static native void addZSTDMultiDictCompress(long cppInfo, long cppColumn, long cppMatchColumn, long[] values, byte[] dictIds);
    private static native void enableReplaceCompression(long cppInfo);

    public interface CompressionFilter {
        /**
         * Triggered at any time when WCDB needs to know whether a table in the current database needs to compress data,
         * mainly including creating a new table, reading and writing a table,and starting to compress a new table.
         * If the current table does not need to compress data, you don't need to config CompressionInfo.
         * @param info An object used to configure the compression.
         */
        void filterCompress(@NotNull CompressionInfo info);
    }

    private static void filterCompress(CompressionFilter filter, long cppInfo, String table) {
        CompressionInfo info = new CompressionInfo(cppInfo, table);
        filter.filterCompress(info);
    }

    /**
     * Configure which tables in the current database need to compress data.
     * Once configured, newly written data will be compressed immediately and synchronously,
     * and you can use {@link Database#stepCompression()} and {@link Database#enableAutoCompression(boolean)} to compress existing data.
     * Note that you need to use this method to configure the compression before executing any statements on current database.
     * @param filter A filter to config compression.
     */
    public void setCompression(@Nullable CompressionFilter filter) {
        setCompression(cppObj, filter);
    }

    private static native void setCompression(long self, CompressionFilter filter);

    /**
     * Configure not to compress new data written to the current database.
     * This configuration is mainly used to deal with some emergency scenarios.
     * It allows already compressed data to be read normally, but new data is no longer compressed.
     * @param disable disable compression or not.
     */
    public void disableCompressNewData(boolean disable) {
        disableCompressNewData(cppObj, disable);
    }

    private static native void disableCompressNewData(long self, boolean disable);

    /**
     * Manually compress 100 rows of existing data.
     * You can call this method periodically until all data is compressed.
     * @throws WCDBException if any error occurs.
     */
    public void stepCompression() throws WCDBException {
        if(!stepCompression(cppObj)){
            throw createException();
        }
    }

    private static native boolean stepCompression(long self);

    /**
     * Configure the database to automatically compress 100 rows of existing data every two seconds.
     *
     * @param enable A flag to enable auto-compression.
     */
    public void enableAutoCompression(boolean enable) {
        enableAutoCompression(cppObj, enable);
    }

    private static native void enableAutoCompression(long self, boolean enable);

    public interface CompressionNotification {
        /**
         * Triggered when a table is compressed completely.
         * When a table is compressed successfully, tableName will be not null.
         * When a database is totally compressed, tableName will be null.
         * @param database The compressing database.
         * @param tableName The compressed table.
         */
        void onCompressed(@NotNull Database database, @Nullable String tableName);
    }

    private static void onTableCompressed(CompressionNotification notification, long cppDatabase, String table) {
        Database database = new Database();
        database.cppObj = cppDatabase;
        notification.onCompressed(database, table);
    }

    /**
     * Register a callback for compression notification.
     * The callback will be called when each table completes the compression.
     * @param notification The notification callback.
     */
    public void setNotificationWhenCompressed(@Nullable CompressionNotification notification) {
        setNotificationWhenCompressed(cppObj, notification);
    }

    private static native void setNotificationWhenCompressed(long self, CompressionNotification notification);

    /**
     * Check if all tables in the database has finished compression.
     * It only check an internal flag of database.
     * @return true if all tables in the database has finished compression.
     */
    public boolean isCompressed() {
        return isCompressed(cppObj);
    }

    private static native boolean isCompressed(long self);

    /**
     * Decompress all compressed data in the database and resave them.
     * It will clear all compression status and progress, and disables automatic compression.
     * If the rollback process is interrupted or failed, the data may be in a mixed state of compressed and uncompressed.
     * @param monitor A progress monitor.
     * @throws WCDBException if any error occurs.
     */
    public void rollbackCompression(@Nullable ProgressMonitor monitor) throws WCDBException {
        if(!rollbackCompression(cppObj, monitor)) {
            throw createException();
        }
    }

    private static native boolean rollbackCompression(long self, ProgressMonitor monitor);

    @Override
    boolean autoInvalidateHandle() {
        return true;
    }

    @Override
    Database getDatabase() {
        return this;
    }
}
